/*
 * Copyright 2007-2008 Andrew O'Malley
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package ws.quokka.plugin.maven;

import org.apache.tools.ant.BuildException;

import ws.quokka.core.bootstrap_util.Assert;
import ws.quokka.core.bootstrap_util.IOUtils;
import ws.quokka.core.bootstrap_util.Logger;
import ws.quokka.core.bootstrap_util.NullLogger;
import ws.quokka.core.plugin_spi.support.AbstractPlugin;
import ws.quokka.core.repo_resolver.Resolver;
import ws.quokka.core.repo_spi.RepoArtifactId;
import ws.quokka.core.repo_spi.Repository;
import ws.quokka.core.repo_spi.RepositoryAware;
import ws.quokka.core.repo_spi.RepositoryFactory;
import ws.quokka.core.repo_spi.RepositoryFactoryAware;
import ws.quokka.core.util.Strings;

import java.io.File;
import java.io.UnsupportedEncodingException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;


/**
 * MavenPlugin provides support for migrating from Maven
 */
public class MavenPlugin extends AbstractPlugin implements RepositoryAware, RepositoryFactoryAware {
    //~ Static fields/initializers -------------------------------------------------------------------------------------

    private static final String MAPPING_TEMPLATE = "<?xml version=\"1.0\"?>\n" + "<mappings>\n" //
        + "    <id-mappings>\n" //
        + "        <!-- \n" //
        + "             Mappings from maven ids to quokka ids.\n" //
        + "             All artifacts must have a matching mapping. This is because quokka groups have\n" //
        + "             a specific meaning, i.e. all artifacts within a group should be released at the \n" //
        + "             same time with the same version.\n" //
        + "             Also, by convention quokka groups do not include the top level qualifier.\n" //
        + "\n" //
        + "             e.g. Hibernate modules are released separately, so the mappings would be:\n" //
        + "                 org.hibernate:hibernate-annotations -> hibernate.annotations\n" //
        + "                 org.hibernate:hibernate-tools -> hibernate.tools\n" //
        + "\n" //
        + "             e.g. Jetty splits a single release into separate modules:\n" //
        + "                 org.mortbay.jetty:jetty -> mortbay.jetty\n" //
        + "                 org.mortbay.jetty:jetty-naming -> mortbay.jetty:naming\n" //
        + "                  \n"
        + "             The minimum mapping is groupId:artifactId -> group (name is assumed to be the last segment of the group)\n" //
        + "             At its most specific a mapping can include full ids (with types left blank).\n" //
        + "             i.e. groupId:artifactId::classifier:version -> group:name::version\n" //
        + "       \n" //
        + "             Quokka searches for the most specific mapping first.\n" //
        + "        -->\n" //
        + "        <!-- <id-mapping from=\"groupId:artifactId::classifier:version\" to=\"group:name::version\"/> -->\n" //
        + "    </id-mappings>\n" //
        + "    <type-mappings>\n" //
        + "        <!-- \n" //
        + "             Mappings from maven types to quokka types.\n" //
        + "             If the types match, there is no need to supply a mapping\n" //
        + "        -->\n" //
        + "        <!-- <type-mapping from=\"maventype\" to=\"quokkatype\"/> -->\n" //
        + "    </type-mappings>\n" //
        + "</mappings>";

    //~ Instance fields ------------------------------------------------------------------------------------------------

    private IOUtils ioUtils = new IOUtils();
    private Repository repository;
    private RepositoryFactory repositoryFactory;

    //~ Methods --------------------------------------------------------------------------------------------------------

    public void setRepositoryFactory(RepositoryFactory repositoryFactory) {
        this.repositoryFactory = repositoryFactory;
    }

    public void setRepository(Repository repository) {
        this.repository = repository;
    }

    private List getIds(boolean mandatory) {
        File ids = properties().getFile("ids", null);
        String id = properties().getString("id", null);
        Assert.isTrue(!mandatory || ((id == null) && (ids != null)) || ((id != null) && (ids == null)),
            "Either 'id' or 'ids' properties must be set (but not both)");

        return (id != null) ? Collections.singletonList(id) : readLines(ids);
    }

    public void importIt() {
        String repositoryId = properties().getString("repository", null);
        Repository repo = (repositoryId == null) ? repository : repositoryFactory.getOrCreate(repositoryId, true);

        MavenImporter importer = createImporter(repo, log());
        List imports = getIds(true);

        try {
            for (Iterator i = imports.iterator(); i.hasNext();) {
                String id = (String)i.next();
                importer.importArtifact(MavenId.parse(id));
            }
        } catch (UnmappedArtifactException e) {
            throw new BuildException("Mappings (listed above) need to be defined from maven to quokka ids.\n" + "Edit "
                + properties().getFile("localMappings").getPath() + " to define mappings and for more information.");
        }
    }

    private List readLines(File file) {
        List result = new ArrayList();

        if ((file == null) || !file.exists()) {
            return result;
        }

        String[] lines = ioUtils.fileToString(file).split("\\n");

        for (int i = 0; i < lines.length; i++) {
            String line = lines[i].trim();

            if ((line.length() != 0) && !line.startsWith("#")) {
                int index = line.indexOf("//");
                result.add((index == -1) ? line : line.substring(0, index).trim());
            }
        }

        return result;
    }

    private MavenImporter createImporter(Repository repository, Logger logger) {
        Collection existingIds = repository.listArtifactIds(true);

        String template = properties().getString("template", null);
        String mvnCommand = properties().getString("mvn", null);
        String pluginVersion = properties().getString("pluginVersion", null);

        IdMapper idMapper = getIdMapper();

        return new MavenImporter(template, mvnCommand, logger, pluginVersion, idMapper, repository,
            getProjectProperties().getBoolean("force", false), existingIds);
    }

    protected IdMapper getIdMapper() {
        File localMappings = properties().getFile("localMappings");

        if (!localMappings.exists()) {
            // Create a stub to get the user started
            ioUtils.createDir(localMappings.getParentFile());
            ioUtils.stringToFile(MAPPING_TEMPLATE, localMappings);
        }

        IdMapper idMapper = IdMapper.load(localMappings);

        File cacheDir = properties().getFile("cache");
        long expiry = properties().getLong("cacheExpiry");
        List remoteMappings = Strings.commaSepList(properties().getString("remoteMappings"));

        for (Iterator i = remoteMappings.iterator(); i.hasNext();) {
            String remoteMapping = (String)i.next();
            IdMapper remoteMapper = IdMapper.load(getCachedFile(remoteMapping, cacheDir, expiry, "xml"));
            idMapper.merge(remoteMapper);
        }

        return idMapper;
    }

    private File getCachedFile(String url, File cacheDir, long expiry, String extension) {
        String hash;

        try {
            hash = ioUtils.md5String(url.getBytes("UTF8"));
        } catch (UnsupportedEncodingException e) {
            throw new BuildException(e);
        }

        ioUtils.createDir(cacheDir);

        File cached = new File(cacheDir, hash + "." + extension);

        if (!cached.exists() || ((System.currentTimeMillis() - cached.lastModified()) > (expiry * 1000))) {
            // Download the url and cache
            try {
                ioUtils.download(getProject(), ioUtils.createURL(url), null, null, cached);
            } catch (Exception e) {
                if (e.getMessage().startsWith("Can't get") && cached.exists()) {
                    log().warn("Can't get " + url + ", using expired cached version");
                } else {
                    throw new BuildException(e);
                }
            }
        }

        return cached;
    }

    public void verify() {
        List idStrings = getIds(false);
        List ids = new ArrayList();

        for (Iterator i = idStrings.iterator(); i.hasNext();) {
            String id = (String)i.next();
            ids.add(RepoArtifactId.parse(id));
        }

        String repositoryId = properties().getString("repository");
        Repository repository = repositoryFactory.getOrCreate(repositoryId, true);
        Resolver resolver = new Resolver(repository, log());

        MavenImporter importer = createImporter(repository, new NullLogger());
        Verifier verifier = new Verifier(repository, resolver, log(), properties(), getProject(), getVerifiedIssues(),
                importer);
        verifier.verify(ids);
    }

    protected List getVerifiedIssues() {
        List issues = new ArrayList();
        File localIssues = properties().getFile("localVerifiedIssues");

        if (localIssues.exists()) {
            issues.addAll(readLines(localIssues));
        }

        File cacheDir = properties().getFile("cache");
        long expiry = properties().getLong("cacheExpiry");
        List remoteIssues = Strings.commaSepList(properties().getString("remoteVerifiedIssues"));

        for (Iterator i = remoteIssues.iterator(); i.hasNext();) {
            String remoteUrl = (String)i.next();
            issues.addAll(readLines(getCachedFile(remoteUrl, cacheDir, expiry, "txt")));
        }

        return issues;
    }
}
